package com.xqboss.apps.service.punch;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.xqboss.apps.constant.Config;
import com.xqboss.apps.constant.LockKey;
import com.xqboss.apps.domain.equipment.Equipment;
import com.xqboss.apps.domain.member.SysUserNft;
import com.xqboss.apps.domain.punch.*;
import com.xqboss.apps.enums.mq.AppMqDelayEnum;
import com.xqboss.apps.enums.mq.AppMqNormalEnum;
import com.xqboss.apps.enums.user.UserWalletEnum;
import com.xqboss.apps.enums.user.WalletChangeTypeEnum;
import com.xqboss.apps.service.consumer.ConsumerTeamMemberService;
import com.xqboss.apps.service.consumer.ConsumerTeamService;
import com.xqboss.apps.service.equipment.EquipmentService;
import com.xqboss.apps.service.user.SysUserNftService;
import com.xqboss.apps.service.user.SysUserWalletService;
import com.xqboss.apps.service.user.UserIdentityRankService;
import com.xqboss.common.core.mq.MqSender;
import com.xqboss.common.core.redis.lock.LockValue;
import com.xqboss.common.core.redis.lock.RedisLock;
import com.xqboss.common.exception.ServiceException;
import com.xqboss.common.utils.TransactionalManage;
import com.xqboss.system.service.ISysConfigService;
import lombok.SneakyThrows;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Trent
 * @date 2023/8/16
 */
@Service
public class UserPunchService {

    @Autowired
    private PunchUserConfigService punchUserConfigService;

    @Autowired
    private PunchRuleService punchRuleService;

    @Autowired
    private SysUserNftService userNftService;

    @Autowired
    private EquipmentService equipmentService;

    @Autowired
    private SysUserWalletService userWalletService;

    @Autowired
    private PunchUserRecordService punchUserRecordService;

    @Autowired
    private PunchWeakenConfigService punchWeakenConfigService;

    @Autowired
    private ConsumerTeamService teamService;

    @Autowired
    private ConsumerTeamMemberService teamMemberService;

    @Autowired
    private ISysConfigService sysConfigService;

    @Lazy
    @Autowired
    private MqSender mqSender;

    @Autowired
    private TransactionalManage transactionalManage;

    @Autowired
    private UserIdentityRankService userIdentityRankService;

    @Autowired
    private PunchUserSlotService punchUserSlotService;

    /**
     * 用户打卡
     *
     * @return
     */
    @RedisLock(lockKey = LockKey.LOCK_USER_PUNCH)
    public PunchUserRecord punchIn(@LockValue Long userId) throws Exception {
        // 检测是否可以打卡
        PunchUserConfig config = punchUserConfigService.getConfig(userId);
        if (Objects.isNull(config.getPunchRuleId())) {
            throw new ServiceException("暂未配置打卡时段！");
        }
        PunchRule rule = punchRuleService.getById(config.getPunchRuleId());
        if (Objects.isNull(rule) || BooleanUtils.isNotTrue(rule.getSaleable())) {
            throw new ServiceException("暂未配置打卡时段！");
        }
        //时间段内未达到总打卡次数，但是当前装备已打完卡，单击【打卡按钮】提示：当前装备已打完x次卡，请切换装备继续打卡
        // 判断是否已经达到最大打卡次数限制
        long punchCount = punchUserRecordService.countUserRuleNftNumberIdPunch(userId, rule.getId(), config.getNftNumberId(), LocalDate.now());
        if (punchCount >= rule.getPunchCount()) {
            throw new ServiceException("当前装备已打完" + rule.getPunchCount() + "次卡，请切换装备继续打卡！");
        }
        //获取用户等级对应总打卡次数--从新赋值总打卡次数
        Integer rankCount = userIdentityRankService.getUserRankIdentityPunchCount(userId, rule.getPunchCount());
        rule.setPunchCount(rankCount);

        // 判断是否达到打卡时间
        LocalTime nowTime = LocalTime.now();
        if (nowTime.isAfter(rule.getEndTime()) || nowTime.isBefore(rule.getStartTime())) {
            throw new ServiceException("未达到打卡时间！");
        }
        if (Objects.nonNull(config.getNextPunchTime())) {
            if (LocalDateTime.of(LocalDate.now(), nowTime).isBefore(config.getNextPunchTime())) {
                throw new ServiceException("未达到打卡时间！");
            }
        }

        if (Objects.isNull(config.getUserNftId())) {
            throw new ServiceException("暂未配置打卡装备！");
        }
        SysUserNft userNft = userNftService.getById(config.getUserNftId());
        if (Objects.isNull(userNft) || !Objects.equals(userNft.getUserId(), userId)) {
            throw new ServiceException("您还未配置打卡装备！");
        }

        Equipment equipment = equipmentService.getByNftNumberId(userNft.getNftNumberId());

        // 判断装备是否已经破损
        // 获取用户打卡队伍
        Long teamId = teamMemberService.getTeamIdByUserId(userId);
        // 打卡收益计算
        PunchUserRecord record = createRecord(config, equipment);
        record.setPunchRuleId(rule.getId());
        record.setUserId(userId);
        record.setNftNumberId(userNft.getNftNumberId());
        record.setTeamId(teamId);
        record.setType(config.getType());

        transactionalManage.executeWithException(() ->  {
            // 存储打卡记录
            boolean save = punchUserRecordService.save(record);
            if (!save) {
                throw new ServiceException("系统繁忙");
            }

            //装备扣除消耗
            equipmentService.deductRecord(record);

            // 增加用户打卡积分
            userWalletService.increase(userId, UserWalletEnum.INTEGRAL, record.getIncome(), WalletChangeTypeEnum.PUNCH, record.getId(),  WalletChangeTypeEnum.PUNCH.getMsg());

            // 计算下次打卡时间
            punchUserConfigService.updateConfigByRecord(config, rule, record);
            return true;
        });

        mqSender.send(AppMqNormalEnum.PUNCH_IN, ListUtil.of(record.getId()));
        int between = (int) LocalDateTimeUtil.between(LocalDateTime.now(), config.getNextPunchTime(), ChronoUnit.SECONDS);
        if(between > 60){
            mqSender.sendDelay(AppMqDelayEnum.PUNCH_COUNTDOWN, config.getId(), config.getNextPunchTime().plusMinutes(-1));
        }
        return record;
    }

    /**
     * 创建打卡记录
     * @return
     */
    private PunchUserRecord createRecord(PunchUserConfig config, Equipment equipment) {
        PunchUserRecord record = new PunchUserRecord();
        record.setNftNumberId(equipment.getNftNumberId());
        record.setLuck(equipment.getLuck());
        record.setStrong(equipment.getStrong());
        record.setDurable(equipment.getDurable());
        record.setTalent(equipment.getTalent());

        // 打卡收益 = 幸运值 * 收益折算系数 * 耐久度削弱比列
        // 幸运值 ×收益折算系数
        BigDecimal incomeRatio = Optional.ofNullable(equipment.getIncomeRatio())
                .orElse(sysConfigService.getValue(Config.Sys.Punch.收益折算系数));
        record.setIncomeRatio(incomeRatio);
        BigDecimal currentIncome = calcPunchIncome(equipment, true);
        // 判断是否设置日打卡上限
        BigDecimal dayIncomeMax = sysConfigService.getValue(Config.Sys.Punch.日打卡收益上限);
        // 是否已经达到打卡上限
        boolean isDayMax = false;
        if(Objects.nonNull(dayIncomeMax)){
            // 计算今日剩余可获得收益
            // 获取今日打卡的收益
            BigDecimal todayIncome = punchUserRecordService.getUserTotalIncome(config.getUserId(), null, LocalDate.now());
            BigDecimal subIncome = dayIncomeMax.subtract(todayIncome);
            if(subIncome.compareTo(BigDecimal.ZERO) <= 0){
                // 今日打卡已经上限
                currentIncome = BigDecimal.ZERO;
                isDayMax = true;
            }else{
                // 剩余收益 小于 当前打卡收益时， 以剩余收益为准
                if(subIncome.compareTo(currentIncome) < 0){
                    currentIncome = subIncome;
                }
            }
        }
        record.setIncome(currentIncome);

        // 耐久消耗： 装备属性总点数 × 耐久消耗系数
        BigDecimal durableRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
        // 计算耐久度破损度消耗
        BigDecimal durabilityUsd;
        BigDecimal breakageUsd;
        if(isDayMax){
            durabilityUsd = BigDecimal.ZERO;
            breakageUsd = BigDecimal.ZERO;
        }else{
            durabilityUsd = durableRatio.multiply(BigDecimal.valueOf(equipment.getTotalAttrCount())).setScale(2, RoundingMode.DOWN);
            breakageUsd = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
        }
        record.setDurabilityRatio(durableRatio);
        record.setDurabilityUse(durabilityUsd);
        record.setBreakageUse(breakageUsd);

        if (equipment.getBreakage().doubleValue() < record.getBreakageUse().doubleValue()) {
            // 破损度判断
            throw new ServiceException("装备损坏，换个装备吧~");
        }
        if (equipment.getDurability().doubleValue() < record.getDurabilityUse().doubleValue()) {
            // 耐久度判断
            throw new ServiceException("耐久度不足，快去修复吧~");
        }
        return record;
    }

    /**
     * 计算装备打卡收益
     * @param equipmentId 装备ID
     * @return
     */
    public BigDecimal calcPunchIncome(Long equipmentId) {
        Equipment equipment = equipmentService.getById(equipmentId);
        return calcPunchIncome(equipment, true);
    }

    /**
     * 计算打卡收益
     * @param equipment 装备
     * @param needCalcWeaken 是否需要计算削弱
     * @return
     */
    public BigDecimal calcPunchIncome(Equipment equipment, boolean needCalcWeaken) {
        BigDecimal defaultIncomeRatio = sysConfigService.getValue(Config.Sys.Punch.收益折算系数);
        PunchWeakenConfig weakenConfig = null;
        if (needCalcWeaken) {
            // 计算耐久度比例削弱
            BigDecimal durabilityRate = equipment.getDurability().divide(equipment.getTotalDurability(), 2, RoundingMode.UP);
            weakenConfig = punchWeakenConfigService.getPunchWeakenConfig(durabilityRate);
        }
        return calcPunchIncome(equipment, defaultIncomeRatio, weakenConfig, needCalcWeaken);
    }

    /**
     * 计算打卡收益
     * @param equipment 装备
     * @param needCalcWeaken 是否需要计算削弱
     * @return
     */
    public BigDecimal calcPunchIncome(Equipment equipment, BigDecimal defaultIncomeRatio, PunchWeakenConfig weakenConfig, boolean needCalcWeaken) {
        BigDecimal incomeRatio = Optional.ofNullable(equipment.getIncomeRatio()).orElse(defaultIncomeRatio);
        BigDecimal income = incomeRatio.multiply(BigDecimal.valueOf(equipment.getLuck()));
        if(needCalcWeaken && Objects.nonNull(weakenConfig)){
            income = income.multiply(weakenConfig.getIncomeRate()).setScale(2, RoundingMode.UP);
        }
        return income;
    }

    /**
     * 获取用户是否已经达到打卡上限
     * @param userId
     * @return
     */
    public boolean isReachingDayMax(Long userId) {
        BigDecimal dayIncomeMax = sysConfigService.getValue(Config.Sys.Punch.日打卡收益上限);
        // 是否已经达到打卡上限
        if(Objects.nonNull(dayIncomeMax)){
            // 计算今日剩余可获得收益
            // 获取今日打卡的收益
            BigDecimal todayIncome = punchUserRecordService.getUserTotalIncome(userId, null, LocalDate.now());
            // 判断今日打开是否已经达到上限
            if(todayIncome.compareTo(dayIncomeMax) >= 0){
                return true;
            }
        }
        return false;
    }

    /**
     * 计算装备耐久度消耗（耐久消耗 = 装备属性总点数 * 耐久消耗系数）
     * @param equipment
     * @return
     */
    public BigDecimal calcDurabilityUsd(Equipment equipment){
        BigDecimal durableRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
        return calcDurabilityUsd(equipment, durableRatio);
    }

    /**
     * 计算装备耐久度消耗（耐久消耗 = 装备属性总点数 * 耐久消耗系数）
     * @param equipment
     * @param defaultDurableRatio
     * @return
     */
    public BigDecimal calcDurabilityUsd(Equipment equipment, BigDecimal defaultDurableRatio){
        BigDecimal durabilityUsd = defaultDurableRatio.multiply(BigDecimal.valueOf(equipment.getTotalAttrCount())).setScale(2, RoundingMode.DOWN);
        return durabilityUsd;
    }

    /**
     * 根据破损值计算装备打卡次数
     * 打卡次数 ＝ 破损值 ÷ 消耗破损值
     */
    public int calcPunchCountByBreakage(BigDecimal breakage) {
        BigDecimal breakageUse = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
        // 向下取整，不管小数点后面有多少位，都直接舍去
        return breakage.divide(breakageUse, 0, RoundingMode.DOWN).intValue();
    }


    /**
     * 计算装备剩余价值/打卡总收益(计算耐久度削弱)
     * @param equipment 装备
     * @param isTotal 是否计算总收益
     */
    public BigDecimal calcPunchIncomeByWeaken(Equipment equipment, boolean isTotal) {
        Equipment newEqu = BeanUtil.copyProperties(equipment, Equipment.class);
        //获取打卡规则中的一轮打卡次数，如果规则为空则默认一轮打卡总次数为5次
        Integer rule = sysConfigService.getValue(Config.Sys.Team.单人当日可打卡次数);
        // 默认消耗破损值
        BigDecimal defaultBreakageUse = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
        // 默认耐久消耗系数
        BigDecimal defaultDurableRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
        // 一天打卡的总收益列表
        List<BigDecimal> incomeVos = new ArrayList<>();

        if (isTotal) {
            // 装备总打卡收益
            //获取装备总打卡次数
            int totalPunchCount = newEqu.getTotalBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
            // 外层循环的次数(天) = 装备总打卡次数 除以 打卡规则一轮打卡次数（向下取整）
            int day = totalPunchCount / rule;
            // 内层循环的次数 = 打卡规则一轮打卡次数；每计算一次打卡收益装备的耐久度都会减少并从新赋值耐久度
            for (int j = 0; j < rule; j++) {
                BigDecimal income = calcPunchIncome(newEqu, true);
                // 每次打卡耐久度都会减少
                newEqu.setDurability(newEqu.getDurability().subtract(calcDurabilityUsd(newEqu, defaultDurableRatio)));
                incomeVos.add(income);
            }
            // 一天的收益列表和 乘以 打卡天数
            BigDecimal sum = incomeVos.stream()
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            return sum.multiply(BigDecimal.valueOf(day));

        } else {
            // 装备剩余打卡收益
            // 获取装备剩余打卡次数
            int punchCount = newEqu.getBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
            //计算能打卡的天数
            int day = punchCount / rule;
            // 如果打卡天数大于1，默认装备的耐久度为总耐久度
            if (day > 1) {
                newEqu.setDurability(newEqu.getTotalDurability());
            }
            for (int i = 0; i < rule; i++) {
                BigDecimal income = calcPunchIncome(newEqu, true);
                newEqu.setDurability(newEqu.getDurability().subtract(calcDurabilityUsd(newEqu, defaultDurableRatio)));
                incomeVos.add(income);
            }
            // 一天的收益列表和 乘以 打卡天数
            BigDecimal sum = incomeVos.stream()
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            sum = sum.multiply(BigDecimal.valueOf(day));

            // a: punchCount % rule.getPunchCount() 取余数，如果punchCount能整除rule.getPunchCount()，则余数为0，否则余数为punchCount除以rule.getPunchCount()的余数
            int remainingPunchCount = punchCount % rule; // 计算剩余的打卡次数的收益
            if (remainingPunchCount > 0) {
                // 正序取
                List<BigDecimal> firstIncomes = incomeVos.stream()
                        .limit(remainingPunchCount) // 只取前remainingPunchCount个元素
                        .collect(Collectors.toList());
                BigDecimal firstSum = firstIncomes.stream()
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                sum = sum.add(firstSum);
                //倒序取的方法
//                List<BigDecimal> lastIncomes = incomeVos.stream()
//                        .skip(Math.max(0, incomeVos.size() - remainingPunchCount)) // 跳过除了最后remainingPunchCount个元素的所有元素
//                        .collect(Collectors.toList());
//                BigDecimal lastSum = lastIncomes.stream()
//                        .reduce(BigDecimal.ZERO, BigDecimal::add);
//                sum = sum.add(lastSum);
            }
            return sum;
        }
    }

    /**
     * 计算装备剩余价值/打卡总收益(计算耐久度削弱)V2
     *
     * @param equipment 装备
     * @param isTotal   是否计算总收益
     */
    public BigDecimal calcPunchIncomeByWeakenV2(Equipment equipment, BigDecimal defaultIncomeRatio, BigDecimal defaultBreakageUse, BigDecimal defaultDurableRatio, PunchWeakenConfig weakenConfig, boolean isTotal) {
        Equipment newEqu = BeanUtil.copyProperties(equipment, Equipment.class);
        //获取打卡规则中的一轮打卡次数，如果规则为空则默认一轮打卡总次数为5次
        Integer rule = sysConfigService.getValue(Config.Sys.Team.单人当日可打卡次数);
        // 一天打卡的总收益列表
        List<BigDecimal> incomeVos = new ArrayList<>();

        if (isTotal) {
            // 装备总打卡收益
            //获取装备总打卡次数
            int totalPunchCount = newEqu.getTotalBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
            // 外层循环的次数(天) = 装备总打卡次数 除以 打卡规则一轮打卡次数（向下取整）
            int day = totalPunchCount / rule;
            // 内层循环的次数 = 打卡规则一轮打卡次数；每计算一次打卡收益装备的耐久度都会减少并从新赋值耐久度
            for (int j = 0; j < rule; j++) {
                BigDecimal income = calcPunchIncome(newEqu, defaultIncomeRatio, weakenConfig, true);
                // 每次打卡耐久度都会减少
                newEqu.setDurability(newEqu.getDurability().subtract(calcDurabilityUsd(newEqu, defaultDurableRatio)));
                incomeVos.add(income);
            }
            // 一天的收益列表和 乘以 打卡天数
            BigDecimal sum = incomeVos.stream()
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            return sum.multiply(BigDecimal.valueOf(day));

        } else {
            // 装备剩余打卡收益
            // 获取装备剩余打卡次数
            int punchCount = newEqu.getBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
            //计算能打卡的天数
            int day = punchCount / rule;
            // 如果打卡天数大于1，默认装备的耐久度为总耐久度
            if (day > 1) {
                newEqu.setDurability(newEqu.getTotalDurability());
            }
            for (int i = 0; i < rule; i++) {
                BigDecimal income = calcPunchIncome(newEqu, defaultIncomeRatio, weakenConfig, true);
                newEqu.setDurability(newEqu.getDurability().subtract(calcDurabilityUsd(newEqu, defaultDurableRatio)));
                incomeVos.add(income);
            }
            // 一天的收益列表和 乘以 打卡天数
            BigDecimal sum = incomeVos.stream()
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            sum = sum.multiply(BigDecimal.valueOf(day));

            // a: punchCount % rule.getPunchCount() 取余数，如果punchCount能整除rule.getPunchCount()，则余数为0，否则余数为punchCount除以rule.getPunchCount()的余数
            int remainingPunchCount = punchCount % rule; // 计算剩余的打卡次数的收益
            if (remainingPunchCount > 0) {
                // 正序取
                List<BigDecimal> firstIncomes = incomeVos.stream()
                        .limit(remainingPunchCount) // 只取前remainingPunchCount个元素
                        .collect(Collectors.toList());
                BigDecimal firstSum = firstIncomes.stream()
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                sum = sum.add(firstSum);
            }
            return sum;
        }
    }

    /**
     * 计算装备剩余价值/打卡总收益(计算耐久度削弱)
     * @param equipmentList 装备
     * @param isTotal 是否计算总收益
     */
    public void calcPunchIncomeByWeakenBatch(List<Equipment> equipmentList, boolean isTotal) {
        //获取打卡规则中的一轮打卡次数，如果规则为空则默认一轮打卡总次数为5次
        PunchRule rule = punchRuleService.getById(1L);
        if (Objects.isNull(rule)) {
            rule = new PunchRule();
            rule.setPunchCount(5);
        }
        // 默认消耗破损值
        BigDecimal defaultBreakageUse = sysConfigService.getValue(Config.Sys.Punch.消耗破损值);
        // 默认耐久消耗系数
        BigDecimal defaultDurableRatio = sysConfigService.getValue(Config.Sys.Punch.耐久消耗系数);
        // 默认收益折算系数
        BigDecimal defaultIncomeRatio = sysConfigService.getValue(Config.Sys.Punch.收益折算系数);
        // 耐久度削弱比列
        List<PunchWeakenConfig> weakenConfigList = punchWeakenConfigService.lambdaQuery()
                .orderByAsc(PunchWeakenConfig::getDurabilityRate)
                .list();
        for (Equipment equipment : equipmentList) {
            // 一天打卡的总收益列表
            List<BigDecimal> incomeVos = new ArrayList<>();
            if (isTotal) {// 装备总打卡收益
                //获取装备总打卡次数
                int totalPunchCount = equipment.getTotalBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
                // 外层循环的次数(天) = 装备总打卡次数 除以 打卡规则一轮打卡次数（向下取整）
                int day = totalPunchCount / rule.getPunchCount();
                // 内层循环的次数 = 打卡规则一轮打卡次数；每计算一次打卡收益装备的耐久度都会减少并从新赋值耐久度
                for (int j = 0; j < rule.getPunchCount(); j++) {
                    // 大于等于当前耐久度的最小耐久度限制
                    // 如：当前耐久度为80， 配置里有90、80、70， 则取80
                    PunchWeakenConfig weakenConfig = weakenConfigList.stream()
                            .filter(t -> t.getDurabilityRate().compareTo(equipment.getDurability().divide(equipment.getTotalDurability(), 2, RoundingMode.UP)) >= 0)
                            .findFirst()
                            .orElse(null);
                    BigDecimal income = calcPunchIncome(equipment, defaultIncomeRatio, weakenConfig, true);
                    // 每次打卡耐久度都会减少
                    equipment.setDurability(equipment.getDurability().subtract(calcDurabilityUsd(equipment, defaultDurableRatio)));
                    incomeVos.add(income);
                }
                // 一天的收益列表和 乘以 打卡天数
                BigDecimal sum = incomeVos.stream()
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                equipment.setRemainingAirdrop(sum.multiply(BigDecimal.valueOf(day)));
            } else {// 装备剩余打卡收益
                // 获取装备剩余打卡次数
                int punchCount = equipment.getBreakage().divide(defaultBreakageUse, 0, RoundingMode.DOWN).intValue();
                //计算能打卡的天数
                int day = punchCount / rule.getPunchCount();
                // 如果打卡天数大于1，默认装备的耐久度为总耐久度
                if (day > 1) {
                    equipment.setDurability(equipment.getTotalDurability());
                }
                for (int i = 0; i < rule.getPunchCount(); i++) {
                    // 大于等于当前耐久度的最小耐久度限制
                    // 如：当前耐久度为80， 配置里有90、80、70， 则取80
                    PunchWeakenConfig weakenConfig = weakenConfigList.stream()
                            .filter(t -> t.getDurabilityRate().compareTo(equipment.getDurability().divide(equipment.getTotalDurability(), 2, RoundingMode.UP)) >= 0)
                            .findFirst()
                            .orElse(null);
                    BigDecimal income = calcPunchIncome(equipment, defaultIncomeRatio, weakenConfig, true);
                    equipment.setDurability(equipment.getDurability().subtract(calcDurabilityUsd(equipment, defaultDurableRatio)));
                    incomeVos.add(income);
                }
                // 一天的收益列表和 乘以 打卡天数
                BigDecimal sum = incomeVos.stream()
                        .reduce(BigDecimal.ZERO, BigDecimal::add);
                sum = sum.multiply(BigDecimal.valueOf(day));

                // a: punchCount % rule.getPunchCount() 取余数，如果punchCount能整除rule.getPunchCount()，则余数为0，否则余数为punchCount除以rule.getPunchCount()的余数
                int remainingPunchCount = punchCount % rule.getPunchCount(); // 计算剩余的打卡次数的收益
                if (remainingPunchCount > 0) {
                    // 正序取
                    List<BigDecimal> firstIncomes = incomeVos.stream()
                            .limit(remainingPunchCount) // 只取前remainingPunchCount个元素
                            .collect(Collectors.toList());
                    BigDecimal firstSum = firstIncomes.stream()
                            .reduce(BigDecimal.ZERO, BigDecimal::add);
                    sum = sum.add(firstSum);
                }
                equipment.setRemainingAirdrop(sum);
            }
        }
    }

    /**
     * 批量打卡
     * @param userId
     */
    @SneakyThrows
    @RedisLock(lockKey = LockKey.LOCK_USER_PUNCH)
    public void punchBatch(@LockValue Long userId) {
        // 检测是否可以打卡
        PunchUserConfig config = punchUserConfigService.getConfig(userId);
        if (Objects.isNull(config.getPunchRuleId())) {
            throw new ServiceException("暂未配置打卡时段！");
        }
        PunchRule rule = punchRuleService.getById(config.getPunchRuleId());
        if (Objects.isNull(rule) || BooleanUtils.isNotTrue(rule.getSaleable())) {
            throw new ServiceException("暂未配置打卡时段！");
        }

        // 判断是否达到打卡时间
        LocalTime nowTime = LocalTime.now();
        if (nowTime.isAfter(rule.getEndTime()) || nowTime.isBefore(rule.getStartTime())) {
            throw new ServiceException("未达到打卡时间！");
        }
        if (Objects.nonNull(config.getNextPunchTime())) {
            if (LocalDateTime.of(LocalDate.now(), nowTime).isBefore(config.getNextPunchTime())) {
                throw new ServiceException("未达到打卡时间！");
            }
        }
        // 获取用户的卡槽,再过滤出可以打卡的卡槽，校验卡槽中的装备今日打卡次数是否达到上限5次，如果达到上限则删除该卡槽
        List<PunchUserSlot> punchUserSlotList = punchUserSlotService.selectByUserId(userId, 1);
        punchUserSlotList = punchUserSlotList.stream()
                .filter(t -> t.getTodayPunchCount() < rule.getPunchCount()
                        && (t.getExpireTime() == null || t.getExpireTime().isAfter(LocalDateTime.now()))
                        //判断是否已经达到最大打卡次数限制
                        && (punchUserRecordService.countUserRuleNftNumberIdPunch(userId, rule.getId(), t.getNftNumberId(), LocalDate.now())) < rule.getPunchCount())
                .collect(Collectors.toList());
        if (punchUserSlotList.isEmpty()) {
            throw new ServiceException("暂无可用的打卡卡槽！");
        }
        List<PunchUserRecord> punchUserRecordList = new ArrayList<>();
        BigDecimal totalIncome = BigDecimal.ZERO;
        Long teamId = teamMemberService.getTeamIdByUserId(userId);
        Iterator<PunchUserSlot> iterator = punchUserSlotList.iterator();
        while (iterator.hasNext()) {
            PunchUserSlot t = iterator.next();
            Equipment equipment = equipmentService.getByNftNumberId(t.getNftNumberId());
            try {
                PunchUserRecord record = createRecord(config, equipment);
                record.setPunchRuleId(rule.getId());
                record.setUserId(userId);
                record.setNftNumberId(t.getNftNumberId());
                record.setTeamId(teamId);
                record.setType(config.getType());
                t.setTodayPunchCount(t.getTodayPunchCount() + 1);
                t.setTodayPunchIncome(t.getTodayPunchIncome().add(record.getIncome()));
                punchUserRecordList.add(record);
                totalIncome = totalIncome.add(record.getIncome());
            } catch (ServiceException e) {
                iterator.remove(); // 删除当前对象
            }
        }
        // 查出最小的todayPunchCount
        int todayPunchCount = punchUserSlotList.stream().mapToInt(PunchUserSlot::getTodayPunchCount).min().orElse(0);
        List<PunchUserSlot> finalPunchUserSlotList = punchUserSlotList;
        // 重新赋值总收益
        BigDecimal finalTotalIncome = totalIncome;
        transactionalManage.executeWithException(() ->  {
            // 存储打卡记录
            boolean save = punchUserRecordService.saveBatch(punchUserRecordList);
            if (!save) {
                throw new ServiceException("系统繁忙");
            }
            //装备扣除消耗
            for (PunchUserRecord t : punchUserRecordList) {
                equipmentService.deductRecord(t);
            }
            // 修改统计数据
            punchUserSlotService.updateBatchById(finalPunchUserSlotList);
            // 增加用户打卡积分
            userWalletService.increase(userId, UserWalletEnum.INTEGRAL, finalTotalIncome, WalletChangeTypeEnum.PUNCH, config.getId(),  WalletChangeTypeEnum.PUNCH.getMsg());
            // 计算下次打卡时间,保存统计数据
            punchUserConfigService.updateConfig(config, rule, todayPunchCount, finalTotalIncome);
            return true;
        });

        mqSender.send(AppMqNormalEnum.PUNCH_IN, punchUserRecordList.stream().map(PunchUserRecord::getId).collect(Collectors.toList()));
        int between = (int) LocalDateTimeUtil.between(LocalDateTime.now(), config.getNextPunchTime(), ChronoUnit.SECONDS);
        if(between > 60){
            mqSender.sendDelay(AppMqDelayEnum.PUNCH_COUNTDOWN, config.getId(), config.getNextPunchTime().plusMinutes(-1));
        }
    }
}
